#  mpd.py - MPD service modules
#  Copyright (C) 2005-2007 Sham Chukoury <eleusis@xmms.org>
#
#  This library is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2.1 of the License, or (at your option) any later version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.

from service import Service
from Chalyx.base import ProxiedSignal
import mpdclient2
import socket

class MPDSignal(ProxiedSignal):
	"""MPD 'proxy' signal"""
	def __init__(self, name, service, rsig, handler):
		self.active = False
		self._tokens = []
		self.name = name
		self.rsig = rsig
		self.handler = handler
		self.service = service
		self.rtoken = None
		self.service.connect("service_connected", self.on_service_connected, None)

	def activate_proxy(self):
		if not self._check_activate(): return
		if not self.service.connected: return
		#print self.name, "activating..."
		self.rtoken = self.service.connector.connect(self.rsig, self.handler, self.name)
		self.active = True

	def deactivate_proxy(self):
		if not self._check_deactivate(): return
		if self.service.connector:
			self.service.connector.disconnect(self.rtoken)
		self.active = False

	def on_service_connected(self, obj, sigdata, udata):
		connected = sigdata
		if connected and len(self.tokens) > 0:
			self.activate_proxy()
		elif not connected:
			self.deactivate_proxy()


class BufferedTalker(mpdclient2.socket_talker):
	def __init__(self, host, port):
		mpdclient2.socket_talker.__init__(self, host, port)
		self.lineBuf = []

	def get_line(self, useBuffer=True):
		if useBuffer and len(self.lineBuf) > 0:
			self.current_line = self.lineBuf.pop(0)
		return mpdclient2.socket_talker.get_line(self)

	def more_data(self):
		self.sock.setblocking(False)
		try:
			line = self.get_line(useBuffer=False)
		except socket.error:
			self.sock.setblocking(True)
			return False
		self.sock.setblocking(True)
		if line != "":
			self.lineBuf.append(line)
			return True
		return False

# this is an async version of mpdclient2.sender_n_fetcher
class MPDAsyncIPC:
	def __init__(self, sender, fetcher):
		self.sender = sender
		self.fetcher = fetcher
		self.out_func = None
		self.__outQ = []
		self.__progressQ = []

	def _make_msg(self, args):
		class MPDMsg:
			def __init__(self, args):
				assert len(args) == 3
				self.cmd = args[0]
				self.args = args[1]
				self.cbdata = args[2]
			def __repr__(self):
				ret = "cmd: %s\nargs:%s\ncbdata:%s" % \
				      (self.cmd, self.args, self.cbdata)
				return ret
		return MPDMsg(args)

	def set_need_out_fun(self, func):
		self.out_func = func

	def check_out(self):
		# call out_func
		if self.out_func:
			self.out_func(self.want_ioout())

	def ioin(self):
		# read incoming data & dispatch
		ret = None
		# We know MPD always processes commands in the order given...
		done = False
		while not done:
			self.fetcher.talker.done = False
			try:
				msg = self.__progressQ[0]
			except IndexError:
				done = True
				continue
			cmd = msg.cmd
			args = msg.args

			junk, howmany, type, keywords = mpdclient2.get_command(cmd, args)

			if howmany == mpdclient2.ZERO:
				self.fetcher.clear()
			elif howmany == mpdclient2.ONE:
				ret = dict(self.fetcher.one_object(keywords, type))
			elif howmany == mpdclient2.MANY:
				result = self.fetcher.all_objects(keywords, type)
				ret = [dict(x) for x in result]
				self.fetcher.clear()

			# call cb if registered
			if msg.cbdata.has_key("cb"):
				cb = msg.cbdata["cb"]
				udata = None
				if msg.cbdata.has_key("udata"):
					udata = msg.cbdata["udata"]
					cb(ret, udata)
				else:
					cb(ret)
			self.__progressQ.pop(0)
			if not self.fetcher.talker.more_data():
				done = True

	def ioout(self):
		# send pending messages
		while len(self.__outQ) > 0:
			msg = self.__outQ[0]
			cmd = msg.cmd
			args = msg.args
			getattr(self.sender, cmd)(*args)
			self.check_out()
			# TODO: check that command was successfully sent?
			self.__progressQ.append(self.__outQ.pop(0))
		self.check_out()

	def want_ioout(self):
		# True if pending messages
		return len(self.__outQ) > 0

	def get_fd(self):
		return self.sender.talker.sock.fileno()

	def __getattr__(self, cmd):
		return lambda *args, **kwargs: self.queue_cmd(cmd, args, kwargs)

	def queue_cmd(self, cmd, args, kwargs):
		self.__outQ.append(self._make_msg((cmd, args, kwargs)))
		self.check_out()

class MPDPropDict(dict):
	"""
	Emulate xmms2's propdict, convert keys to those that xmms2 clients expect
	"""
	def __init__(self, d):
		self.update(d)
		time = 0
		if self.has_key("time"):
			time = self["time"]
		self["duration"] = int(time) * 1000
		if self.has_key("file") and not self.has_key("id"):
			self["id"] = self["file"]
		if not self.has_key("url"):
			self["url"] = ""

class MPDService(Service, mpdclient2.mpd_connection):
	def __init__(self, connClsData, clientName, cfgHandler, mname="mpd-service"):
		Service.__init__(self, cfgHandler, mname)
		self.config = self.cfgHandler["MPDService"]
		self.__initcfg()
		self.clientName = clientName
		self.connClass = connClsData[0]
		self.connClsArgs = connClsData[1]
		self.connected = False
		self.signals = {"service_connected": []}
		self.signals["pb_playtime"] = MPDSignal("pb_playtime", self, "pb_playtime", self._faketrans)
		self.signals["pb_status"] = MPDSignal("pb_status", self, "pb_status", self._faketrans)
		self.signals["pb_current_id"] = MPDSignal("pb_current_id", self, "pb_current_id", self._faketrans)
		self.signals["pl_pos"] = MPDSignal("pl_pos", self, "pl_pos", self._faketrans)
		self.signals["pl_change"] = MPDSignal("pl_change", self, "pl_change", self._faketrans)
		self.talker = None
		self.send = mpdclient2.command_sender(None)
		self.fetch = mpdclient2.response_fetcher(None)
		self.do = MPDAsyncIPC(self.send, self.fetch)

	def __initcfg(self):
		self.config.register("host", "MPD host", "localhost")
		self.config.register("port", "MPD port", "6600")
		self.config.register("password", "Password", "")
		self.config.register("pingInterval", "Ping interval", "5")

	def s_connect(self, address = None):
		# TODO: catch exception on connect and exit gracefully?
		self.talker = BufferedTalker(self.config["host"], int(self.config["port"]))
		self.send.talker = self.talker
		self.fetch.talker = self.talker
		self.connector = self.connClass(self, float(self.config["pingInterval"]), self.connClsArgs)
		self._hello()
		self.ping()
		password = self.config["password"]
		if password != "":
			self.password(password)
		self.connected = True
		self.connector.status_check(True)
		self.emit("service_connected", self.connected)

	def s_disconnect(self):
		self.connected = False
		del self.talker
		del self.send.talker
		del self.fetch.talker
		self.connector.stop()
		del self.connector
		self.connector = None
		self.talker = None
		self.emit("service_connected", self.connected)

	# TODO: service calls to check for error

	def playback_start(self, cb = None):
		self.play()

	def playback_stop(self, cb = None):
		self.stop()

	def playback_pause(self, cb = None):
		# TODO: pause can be toggled
		self.pause(1)

	def playback_play_pause_toggle(self):
		self.play()
		self.pause(self.connector.mpdState["state"] == "play")

	def playback_status(self, cb):
		def __status(data):
			cb(self.connector.pbstatus_map[data["state"]])
		self.status(cb=__status)
	
	def playback_seek_ms(self, ms):
		self.seek(int(self.connector.mpdState["song"]), int(ms / 1000))
	
	def playlist_list_entries(self, cb, playlist=None):
		# TODO: use optional playlist arg
		def __list_cb(data):
			cb([x["file"] for x in data])
		self.playlistinfo(cb=__list_cb)

	def playlist_current_pos(self, cb, playlist=None):
		# TODO: use optional playlist arg
		def __status_cb(status):
			if not status.has_key("song"):
				cb(0)
			else:
				cb(status["song"])
		if self.connector.mpdState.has_key("song"):
			return self.connector.mpdState["song"]
		else:
			self.status(cb=__status_cb)

	def playlist_play_pos(self, pos):
		self.play(pos)

	def playlist_next(self):
		self.next()

	def playlist_prev(self):
		self.previous()

	def playlist_jump(self, pos):
		self.playlist_play_pos(pos)
	
	def playlist_remove(self, pos):
		self.delete(int(pos))
	
	def playlist_add(self, url):
		# need to make sure url is within mpd music dir
		print "playlist add not implemented"
	
	def playlist_add_from_library(self, id):
		self.add(id)
	
	def playlist_insert(self, pos, url):
		# need to make sure url is within mpd music dir
		print "playlist insert not implemented"
	
	# TODO/FIXME: untested
	def playlist_insert_from_library(self, pos, id):
		def __added_cb(data):
			self.move(self.connector.mpdState["playlistlength"], pos)
		self.add(id, cb=__added_cb)
	
	def playlist_move(self, fromPos, toPos):
		self.move(fromPos, toPos)
	
	def library_get_info(self, id, cb):
		# ASSUME: id is file path in music dir
		def __info_cb(res):
			cb(MPDPropDict(res[0]))
		self.listallinfo(id, cb=__info_cb)
	
	def info_current_stream(self, cb):
		def __id_cb(res):
			info = MPDPropDict({"id": None})
			if len(res) > 0:
				info = MPDPropDict(res)
			cb(info)
		self.currentsong(cb=__id_cb)
